fix(storage): store fork-choice votes independently - #552
Conversation
Greptile SummaryThis PR separates latest fork-choice votes from bounded proof retention.
Confidence Score: 4/5This PR should not merge until vote pruning preserves latest messages whose heads remain relevant above the finalized checkpoint. The new pruning predicate uses the attestation target, while LMD-GHOST consumes its head; after finalization this can remove live-branch voting weight from subsequent head calculations. Files Needing Attention: crates/storage/src/store.rs
|
| Filename | Overview |
|---|---|
| crates/storage/src/store.rs | Adds the independent latest-vote store and ingestion paths, but pruning by target slot can remove votes whose heads remain relevant to fork choice. |
| crates/blockchain/src/store.rs | Records validated block-included attestation votes after persisting the imported block and state. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart LR
A[Known aggregate] --> V[Independent latest-vote store]
B[Block-included attestation] --> V
A --> P[Bounded proof buffer]
V --> F[LMD-GHOST fork choice]
C[Finalization advances] --> R[Prune votes by target slot]
R --> V
Prompt To Fix All With AI
### Issue 1
crates/storage/src/store.rs:1087-1090
**Target-based pruning drops live votes**
When a validator's latest attestation targets a finalized checkpoint but its head remains on a live post-finalization branch, `prune_known_votes` removes the vote based solely on `target.slot`. LMD-GHOST weights `head.root`, so subsequent head calculations lose that validator's weight and can select the wrong branch until a replacement vote arrives.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Reviews (1): Last reviewed commit: "fix(storage): store fork-choice votes in..." | Re-trigger Greptile
| for vote in votes.iter_mut() { | ||
| let should_prune = vote | ||
| .as_ref() | ||
| .is_some_and(|data| data.target.slot <= finalized_slot); |
There was a problem hiding this comment.
Target-based pruning drops live votes
When a validator's latest attestation targets a finalized checkpoint but its head remains on a live post-finalization branch, prune_known_votes removes the vote based solely on target.slot. LMD-GHOST weights head.root, so subsequent head calculations lose that validator's weight and can select the wrong branch until a replacement vote arrives.
Knowledge Base Used:
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/storage/src/store.rs
Line: 1087-1090
Comment:
**Target-based pruning drops live votes**
When a validator's latest attestation targets a finalized checkpoint but its head remains on a live post-finalization branch, `prune_known_votes` removes the vote based solely on `target.slot`. LMD-GHOST weights `head.root`, so subsequent head calculations lose that validator's weight and can select the wrong branch until a replacement vote arrives.
**Knowledge Base Used:**
- [Blockchain core: fork choice, state transition, block building, sync](https://app.greptile.com/lambdaclass/-/custom-context/knowledge-base/lambdaclass/ethlambda/-/docs/blockchain-core.md)
- [Storage (`ethlambda_storage`)](https://app.greptile.com/lambdaclass/-/custom-context/knowledge-base/lambdaclass/ethlambda/-/docs/storage.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
MegaRedHand
left a comment
There was a problem hiding this comment.
Looks good
Left some comments about pruning. The issue didn't mention it, but the main issue with how we stored votes is that proof are pruned regularly, which shouldn't affect votes: LMD-GHOST uses the latest message from each validator, so otherwise no pruning should happen.
Also, an additional PR is needed to finish addressing the issue, since we still need to replace the payloads and proofs in fork-choice with the new votes.
| } | ||
|
|
||
| /// Insert proof-less fork-choice votes from block-included attestations. | ||
| pub fn insert_known_attestation_votes(&mut self, attestations: &[AggregatedAttestation]) { |
There was a problem hiding this comment.
Can we embed this inside insert_signed_block?
| type StorageKey = Vec<u8>; | ||
| type StorageEntry = (StorageKey, Vec<u8>); | ||
| type BlockRootIndexChanges = (Vec<StorageKey>, Vec<StorageEntry>); | ||
| type VoteStore = Vec<Option<AttestationData>>; |
There was a problem hiding this comment.
Let's make this a HashMap<usize, AttestationData> instead. That'll make this easier to reason about, and avoid performance edge-cases.
| } | ||
|
|
||
| /// Prune latest fork-choice votes whose target is at or below `finalized_slot`. | ||
| pub fn prune_known_votes(&mut self, finalized_slot: u64) -> usize { |
There was a problem hiding this comment.
Pruning isn't needed for votes, since we always keep the last vote of each validator only
| /// Latest fork-choice vote per validator, independent from bounded proof buffers. | ||
| votes: Arc<Mutex<VoteStore>>, |
There was a problem hiding this comment.
Let's wrap the VoteStore with a ForkChoiceState struct, so we can later extend it as needed without having a mutex for each field.
Also, another thing I forgot about is that we have to store both known and new votes (each in a separate map). Here we are storing known votes only.
| } | ||
|
|
||
| /// Insert a single proof into the known (fork-choice-active) buffer. | ||
| pub fn insert_known_aggregated_payload( |
There was a problem hiding this comment.
This function isn't used anywhere, right? Let's remove it.
| fn new_vote_store() -> Arc<Mutex<VoteStore>> { | ||
| Arc::new(Mutex::new(Vec::new())) | ||
| } |
There was a problem hiding this comment.
This is just Default::default(), right?
🗒️ Description / Motivation
This PR fixes fork-choice vote tracking so validator votes are stored independently from aggregated proofs and gossip signatures.
Previously, fork-choice votes were derived from bounded in-memory proof/signature buffers. When those buffers evicted old entries through FIFO cleanup, a validator’s latest vote could disappear even though the vote should still count for fork choice. This PR adds a separate proof-less vote store so fork-choice weights are not affected by proof/signature retention.
What Changed
Updated
crates/storage/src/store.rsUpdated
crates/blockchain/src/store.rsCorrectness / Behavior Guarantees
Tests Added / Run
Related Issues / PRs
✅ Verification Checklist
make fmt— cleanmake lint(clippy with-D warnings) — cleanmake test(cargo test --workspace --profile release-fast) — all passing